Zbiór danych “Adult” jest jednym z klasycznych zestawów danych używanych w dziedzinie uczenia maszynowego, szczególnie w kontekście klasyfikacji binarnej. Dane te zostały pierwotnie wyodrębnione z bazy danych spisu powszechnego z 1994 roku przez US Census Bureau. Celem zestawu jest przewidywanie, czy dochód osoby przekracza 50 tysięcy dolarów rocznie na podstawie szeregu cech demograficznych i ekonomicznych, takich jak wiek, edukacja, status małżeński, czy liczba godzin pracy tygodniowo. Zbiór ten zawiera 48 842 obserwacje oraz 14 atrybutów, w tym 6 ciągłych i 8 nominalnych. Analiza tych danych stanowi interesujące wyzwanie ze względu na nierównomierne rozkłady klas oraz obecność braków danych w niektórych atrybutach.
W projekcie wykorzystane zostaną współczesne metody eksploracyjnej analizy danych (EDA) oraz techniki uczenia maszynowego w celu przeprowadzenia skutecznej klasyfikacji. Projekt będzie również uwzględniał aspekty interpretowalności modelu, co pozwoli na lepsze zrozumienie wpływu poszczególnych zmiennych na przewidywaną zmienną docelową.
Cel pracy:
Celem niniejszej pracy jest opracowanie skutecznego modelu klasyfikacyjnego do przewidywania poziomu dochodu (>50K lub <=50K) na podstawie dostarczonego zbioru danych “Adult”. Aby osiągnąć ten cel, praca skupi się na kilku kluczowych etapach:
Eksploracyjna analiza danych (EDA): Przeprowadzenie wstępnej analizy, w tym opisanie i wizualizacja danych, obliczenie podstawowych statystyk oraz zbadanie relacji między zmiennymi niezależnymi a zmienną docelową.
Przygotowanie danych: Zajęcie się brakami danych, wartościami odstającymi oraz potencjalną modyfikacją zmiennych, tak aby dane były odpowiednie do użycia w modelach uczenia maszynowego.
Uczenie maszynowe: Zastosowanie przynajmniej trzech różnych algorytmów klasyfikacyjnych, w tym SVM oraz drzew decyzyjnych, w celu porównania ich efektywności.
Ocena wyników: Analiza jakości klasyfikacji oraz interpretowalności najlepszego modelu przy użyciu narzędzi takich jak wykresy ceteris-paribus, wykresy częściowej zależności czy wartości SHAP.
Wnioski: Sformułowanie wniosków dotyczących skuteczności zastosowanych metod oraz znaczenia poszczególnych zmiennych dla przewidywania dochodu.
Praca ma na celu nie tylko opracowanie skutecznego modelu, ale również dogłębne zrozumienie danych oraz praktyczne zastosowanie metod eksploracyjnych i modelowania.
Do przeprowadzenia analizy danych i budowy modeli klasyfikacyjnych wykorzystano szeroki zestaw bibliotek:
import pandas as pd
import numpy as np
from IPython.display import Markdown as md # type: ignore
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix, recall_score, accuracy_score, roc_auc_score
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import GridSearchCV
import warnings
from sklearn.exceptions import ConvergenceWarning
warnings.filterwarnings("ignore", category=ConvergenceWarning)
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import ConfusionMatrixDisplay
from sklearn.tree import plot_tree
from utils import *
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import roc_curve, auc
from IPython.display import display
import plotly.express as px
import shap
from scipy.stats import skew
from scipy.stats import pointbiserialr, spearmanr, pearsonr
Na potrzeby projektu zostału również stworzone dodatkowe funkcje, które miały za zadanie przyspieszenie i zautomatyzowanie częsci obliczeniowych
def specificity_score(y_true, y_pred):
# Generowanie macierzy pomyłek
cm = confusion_matrix(y_true, y_pred)
# Rozpakowanie wartości z macierzy pomyłek
tn, fp, fn, tp = cm.ravel()
# Obliczenie specyficzności
return tn / (tn + fp)
Poniższa tabela pokazuje surowy zbiór danych, które będą obiektem analizy. W dalszych etapacch zostaną one przeanalizowane i nastepnie odpowiednio przygotowane w celu stworzonenia możliwie najlepszych modeli klasyfikacyjnych
income_data = pd.read_csv("datasets/income.csv")
# Eksport danych do R
r.income_data = income_data
| age | workclass | fnlwgt | education | educational-num | marital-status | occupation | relationship | race | gender | capital-gain | capital-loss | hours-per-week | native-country | income |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 25 | Private | 226802 | 11th | 7 | Never-married | Machine-op-inspct | Own-child | Black | Male | 0 | 0 | 40 | United-States | <=50K |
| 38 | Private | 89814 | HS-grad | 9 | Married-civ-spouse | Farming-fishing | Husband | White | Male | 0 | 0 | 50 | United-States | <=50K |
| 28 | Local-gov | 336951 | Assoc-acdm | 12 | Married-civ-spouse | Protective-serv | Husband | White | Male | 0 | 0 | 40 | United-States | >50K |
| 44 | Private | 160323 | Some-college | 10 | Married-civ-spouse | Machine-op-inspct | Husband | Black | Male | 7688 | 0 | 40 | United-States | >50K |
| 18 | ? | 103497 | Some-college | 10 | Never-married | ? | Own-child | White | Female | 0 | 0 | 30 | United-States | <=50K |
| 34 | Private | 198693 | 10th | 6 | Never-married | Other-service | Not-in-family | White | Male | 0 | 0 | 30 | United-States | <=50K |
| 29 | ? | 227026 | HS-grad | 9 | Never-married | ? | Unmarried | Black | Male | 0 | 0 | 40 | United-States | <=50K |
| 63 | Self-emp-not-inc | 104626 | Prof-school | 15 | Married-civ-spouse | Prof-specialty | Husband | White | Male | 3103 | 0 | 32 | United-States | >50K |
| 24 | Private | 369667 | Some-college | 10 | Never-married | Other-service | Unmarried | White | Female | 0 | 0 | 40 | United-States | <=50K |
| 55 | Private | 104996 | 7th-8th | 4 | Married-civ-spouse | Craft-repair | Husband | White | Male | 0 | 0 | 10 | United-States | <=50K |
Zbiór danych składa się z 48842 obserwacji złożonych z 15 cech, które zostały wypisane poniżej:
print(', '.join(income_data.columns) )
## age, workclass, fnlwgt, education, educational-num, marital-status, occupation, relationship, race, gender, capital-gain, capital-loss, hours-per-week, native-country, income
Zmienne kategoryczne składają się z następujących elementów:
income_data['workclass'].unique()
## array(['Private', 'Local-gov', '?', 'Self-emp-not-inc', 'Federal-gov',
## 'State-gov', 'Self-emp-inc', 'Without-pay', 'Never-worked'],
## dtype=object)
income_data['education'].unique()
## array(['11th', 'HS-grad', 'Assoc-acdm', 'Some-college', '10th',
## 'Prof-school', '7th-8th', 'Bachelors', 'Masters', 'Doctorate',
## '5th-6th', 'Assoc-voc', '9th', '12th', '1st-4th', 'Preschool'],
## dtype=object)
income_data['marital-status'].unique()
## array(['Never-married', 'Married-civ-spouse', 'Widowed', 'Divorced',
## 'Separated', 'Married-spouse-absent', 'Married-AF-spouse'],
## dtype=object)
income_data['occupation'].unique()
## array(['Machine-op-inspct', 'Farming-fishing', 'Protective-serv', '?',
## 'Other-service', 'Prof-specialty', 'Craft-repair', 'Adm-clerical',
## 'Exec-managerial', 'Tech-support', 'Sales', 'Priv-house-serv',
## 'Transport-moving', 'Handlers-cleaners', 'Armed-Forces'],
## dtype=object)
income_data['relationship'].unique()
## array(['Own-child', 'Husband', 'Not-in-family', 'Unmarried', 'Wife',
## 'Other-relative'], dtype=object)
income_data['race'].unique()
## array(['Black', 'White', 'Asian-Pac-Islander', 'Other',
## 'Amer-Indian-Eskimo'], dtype=object)
education_crosstab = pd.crosstab(income_data['education'], income_data['educational-num'])
education_crosstab
## educational-num 1 2 3 4 5 6 ... 11 12 13 14 15 16
## education ...
## 10th 0 0 0 0 0 1389 ... 0 0 0 0 0 0
## 11th 0 0 0 0 0 0 ... 0 0 0 0 0 0
## 12th 0 0 0 0 0 0 ... 0 0 0 0 0 0
## 1st-4th 0 247 0 0 0 0 ... 0 0 0 0 0 0
## 5th-6th 0 0 509 0 0 0 ... 0 0 0 0 0 0
## 7th-8th 0 0 0 955 0 0 ... 0 0 0 0 0 0
## 9th 0 0 0 0 756 0 ... 0 0 0 0 0 0
## Assoc-acdm 0 0 0 0 0 0 ... 0 1601 0 0 0 0
## Assoc-voc 0 0 0 0 0 0 ... 2061 0 0 0 0 0
## Bachelors 0 0 0 0 0 0 ... 0 0 8025 0 0 0
## Doctorate 0 0 0 0 0 0 ... 0 0 0 0 0 594
## HS-grad 0 0 0 0 0 0 ... 0 0 0 0 0 0
## Masters 0 0 0 0 0 0 ... 0 0 0 2657 0 0
## Preschool 83 0 0 0 0 0 ... 0 0 0 0 0 0
## Prof-school 0 0 0 0 0 0 ... 0 0 0 0 834 0
## Some-college 0 0 0 0 0 0 ... 0 0 0 0 0 0
##
## [16 rows x 16 columns]
Zmienne education oraz educational-num przekazują dokładnie te same informacje, wobec tego education zostanie odrzucona.
income_data = income_data.drop('education', axis = 1)
mari_rel_crosstab = pd.crosstab(income_data['marital-status'], income_data['relationship'])
mari_rel_crosstab
## relationship Husband Not-in-family ... Unmarried Wife
## marital-status ...
## Divorced 0 3628 ... 2369 0
## Married-AF-spouse 12 0 ... 0 23
## Married-civ-spouse 19704 23 ... 0 2308
## Married-spouse-absent 0 330 ... 183 0
## Never-married 0 7114 ... 1333 0
## Separated 0 637 ... 668 0
## Widowed 0 851 ... 572 0
##
## [7 rows x 6 columns]
print("V Cramera: " + str(cramers_v(mari_rel_crosstab)))
## V Cramera: 0.48815980396007874
Wartość V wskazuje na silną zależność pomiędzy zmiennymi relationship a marital-status, wobec tego zmienna relationship zostanie odrzucona.
income_data = income_data.drop("relationship", axis = 1)
Pierwotny zbiór danych zawiera pytajniki w postaci stringa jako braki danych, zostanią one zastąpione wartościami NaN, tak by można było z nimi łatwiej pracować
income_data.replace('?', np.nan, inplace=True)
income_data['native-country'].value_counts()
## United-States 43832
## Mexico 951
## Philippines 295
## Germany 206
## Puerto-Rico 184
## Canada 182
## El-Salvador 155
## India 151
## Cuba 138
## England 127
## China 122
## South 115
## Jamaica 106
## Italy 105
## Dominican-Republic 103
## Japan 92
## Guatemala 88
## Poland 87
## Vietnam 86
## Columbia 85
## Haiti 75
## Portugal 67
## Taiwan 65
## Iran 59
## Greece 49
## Nicaragua 49
## Peru 46
## Ecuador 45
## France 38
## Ireland 37
## Hong 30
## Thailand 30
## Cambodia 28
## Trinadad&Tobago 27
## Yugoslavia 23
## Outlying-US(Guam-USVI-etc) 23
## Laos 23
## Scotland 21
## Honduras 20
## Hungary 19
## Holand-Netherlands 1
## Name: native-country, dtype: int64
Przytłaczająca większość obserwacji zawiera USA jako kraj pochodzenia, zmienna native-country zostanie sporwadzona do zmiennej binarnej native-american przyjmującej 1 dla natywnych Amerykan oraz 0 dla imigrantów.
income_data['native-american'] = income_data['native-country'].apply(
lambda x: 1 if x == 'United-States' else (0 if pd.notnull(x) else np.nan)
)
income_data = income_data.drop('native-country', axis = 1)
Zmienne capital-gain oraz capital-loss zostaną zastąpione pojedynczą zmienną - capital-net
income_data['capital-net'] = income_data['capital-gain'] - income_data['capital-loss']
income_data = income_data.drop(['capital-gain', 'capital-loss'], axis = 1)
quantitive_cols = ['age', 'fnlwgt', 'capital-net', 'hours-per-week']
ordinal_cols = ['educational-num', 'income']
categorical_cols = ['workclass', 'marital-status', 'occupation', 'race', 'gender', 'native-american']
variable_stats = []
for quantitive in quantitive_cols:
variable_stats.append(describe_variable(income_data[quantitive], 'quantitive', quantitive))
variable_stats.append(describe_variable(income_data['educational-num'], 'ordinal', 'educational-num'))
for categorical in categorical_cols:
variable_stats.append(describe_variable(income_data[categorical], 'categorical', categorical))
variable_statsDF = pd.DataFrame(variable_stats).set_index("index")
variable_statsDF.T
## index age fnlwgt ... gender native-american
## mean 38.643585 189664.134597 ... nie dotyczy nie dotyczy
## median 37.0 178144.5 ... nie dotyczy nie dotyczy
## mode [36] [203488] ... [Male] [1.0]
## std 13.71051 105604.025423 ... nie dotyczy nie dotyczy
## min 17 12285 ... nie dotyczy nie dotyczy
## max 90 1490400 ... nie dotyczy nie dotyczy
## unique 74 28523 ... 2 2
## missing 0 0 ... 0 857
##
## [8 rows x 11 columns]
percent_counts = income_data['income'].value_counts(normalize=True) * 100
plt.bar(percent_counts.index, percent_counts.values)
plt.title('Procentowy udzial klasy income')
plt.xlabel('Wartosc target')
plt.ylabel('Procentowy udzial (%)')
plt.ylim(0, 100)
## (0.0, 100.0)
plt.show()
Wykres pokazuje wyraźne niezbalansowanie danych - około 3/4 rekordów zawiera dochody mniejsze niż 50 tysięcy.
def show_categoricals():
categorical_colsX = categorical_cols[:-1]
fig, axes = plt.subplots(2, 3, figsize=(20, 10))
axes = axes.flatten()
for i, col in enumerate(categorical_colsX):
if i < len(axes):
sns.countplot(data=income_data, x=col, ax=axes[i])
axes[i].set_title(f'{col}')
axes[i].set_xlabel('')
axes[i].set_ylabel('Liczba rekordow')
axes[i].tick_params(axis='x', rotation=90)
for j in range(len(categorical_colsX), len(axes)):
fig.delaxes(axes[j])
plt.tight_layout()
plt.show()
show_categoricals()
Workclass
income_data['workclass'] = income_data['workclass'].replace(
{'Local-gov': 'other', 'Federal-gov': 'gov', 'Without-pay': 'gov'}
)
income_data['workclass'] = income_data['workclass'].replace(
{'Self-emp-not-inc': 'Other', 'Self-emp-inc': 'Other', 'State-gov': 'Other', 'Never-worked': 'Other'}
)
Marital-status
income_data['marital-status'] = income_data['marital-status'].replace(
{'Widowed': 'Separated', 'Divorced': 'Separated', 'Married-spouse-absent': 'Separated', 'Married-AF-spouse': 'Separated'}
)
Race
income_data['race'] = income_data['race'].replace(
{'Asian-Pac-Islander': 'Other', 'Amer-Indian-Eskimo': 'Other'}
)
show_categoricals()
Ilość klas dla poszczególnych zmiennych kategorycznych została zredukowana do maksymalnie trzech, za wyjątkiem zmiennej occupation, dla której ciężko byłoby o logiczny podział, dlatego przed metodami ML zostanie wykorzystany target-encoding, podczas gdy dla pozostałych zmiennych zostanie zastosowany one-hot-encoding.
plt.figure(figsize=(10, 6))
sns.countplot(data=income_data, x='educational-num')
plt.title('Liczba rekordów dla poszczególnych poziomów edukacji')
plt.xlabel('educational-num')
plt.ylabel('Liczba rekordów')
plt.show()
fig, axes = plt.subplots(1, len(quantitive_cols), figsize=(15, 5), sharey=False)
for i, col in enumerate(quantitive_cols):
sns.boxplot(data=income_data, y=col, ax=axes[i])
axes[i].set_title(f'{col}')
axes[i].set_ylabel('')
plt.tight_layout()
plt.show()
Zmienna age wydaje się być niemal gotowa do wykorzystania metod klasyfikacyjnych - drobnym problemem mogą być pojedyncze outliery.
W przypadku zmiennej fnlwgt występuje ogromna ilość outlierów, być może ma ona nawet rozkład bimodalny.
def kdeplot(var):
sns.kdeplot(data=income_data, x=var, fill=True)
plt.title(f"Rozkład gęstości {var}")
plt.xlabel("Wartości")
plt.ylabel("Gęstość")
plt.show()
kdeplot("fnlwgt")
Szczęśliwie, prognozy okazały się zbyt pesymistyczne - rozkład tej zmiennej jest co prawda mocno przesunięty w prawo i będzie wymagał dodatkowej pracy z danymi, ale nie jest bimodalny, co mogłoby sugerować 2 grupy wśród obserwacji i znaczne skomplikowanie przygotowania danych.
print(skew(income_data['fnlwgt']))
## 1.438847687943433
fnl_mean = income_data['fnlwgt'].mean()
fnl_std = income_data['fnlwgt'].std()
fnl_upper_bound = fnl_mean + 3 * fnl_std
fnl_outliers = income_data[income_data['fnlwgt'] >= fnl_upper_bound]
print(fnl_outliers.shape)
## (506, 12)
print(fnl_outliers[fnl_outliers['income'] == '>50K'].shape)
## (115, 12)
Wśród rekordów, które w głównej mierze powodują skośność dodatnią stosunek zarabiających więcej niż 50000 do tych zarabiających mniej niż 50000 odpowiada temu w całym zbiorze - na tej podstawie usunięcie tych danych prawdopodobnie nie zaszkodzi ogólnej jakości prognoz.
income_data = income_data[income_data['fnlwgt'] < fnl_upper_bound]
print(skew(income_data['fnlwgt']))
## 0.6351318645927562
Po tej operacji wciąż występuje prawostronna skośność, jest ona jednak na znacznie bardziej akceptowalnym poziomie.
kdeplot("fnlwgt")
kdeplot("capital-net")
print(skew(income_data['capital-net']))
## 11.790428808003963
Podobnie jak w przypadku zmiennej fnlwgt występuje tutaj dodatnia skośność. Analogicznie zbadane zostanie, czy wśród outlierów stosunek wartości zmiennej objaśnianej jest zachowany.
capital_mean = income_data['capital-net'].mean()
capital_std = income_data['capital-net'].std()
capital_upper_bound = capital_mean + 3 * capital_std
capital_outliers = income_data[income_data['capital-net'] >= capital_upper_bound]
print(capital_outliers.shape)
## (328, 12)
print(capital_outliers[capital_outliers['income'] == '>50K'].shape)
## (319, 12)
W przypadku zmiennej capital-net wśród outlierów stosunek osób zarabiających więcej niż 50000 do zarabiających mniej jest znacznie wyższy, niż w całym zbiorze danych. Usunięcie wartości mogłoby znacznie pogorszyć jakość przyszłych prognoz. Zamiast tego zastosowana zostanie transformacja logarytmiczna, która przy zachowaniu rekordów pozwoli na zmniejszenie poziomu dodatniej skośności.
income_data = income_data.copy()
income_data['capital-net'] = np.sign(income_data['capital-net']) * np.log1p(abs(income_data['capital-net']))
print(skew(income_data['capital-net']))
## 1.066400422650951
Skośność wciąż występuje, jednak została ona znacznie zredukowana.
kdeplot("capital-net")
Jest to rozkład trójmodalny, co może potencjalnie powodować problemy podczas korzystania z niektórych metod klasyfikacyjnych.
kdeplot("hours-per-week")
print(skew(income_data['hours-per-week']))
## 0.23724635735472097
W przypadku hours-per-week poziom skośnosci jest akceptowalny już dla pierwotnych wartości zmiennej.
hours_mean = income_data['hours-per-week'].mean()
hours_std = income_data['hours-per-week'].std()
hours_upper_bound = hours_mean + 3 * hours_std
hours_lower_bound = hours_mean - 3 * hours_std
hours_outliers_upper = income_data[income_data['hours-per-week'] >= hours_upper_bound]
hours_outliers_lower = income_data[income_data['hours-per-week'] <= hours_lower_bound]
upper_count = hours_outliers_upper.shape[0]
upper_high_income = hours_outliers_upper[hours_outliers_upper['income'] == '>50K'].shape[0]
lower_count= hours_outliers_lower.shape[0]
lower_high_income = hours_outliers_lower[hours_outliers_lower['income'] == '>50K'].shape[0]
print(f"Odsetek zarobków wynoszących więcej niż 50000 wśród górnych outlierów: {upper_high_income/upper_count}")
## Odsetek zarobków wynoszących więcej niż 50000 wśród górnych outlierów: 0.34701492537313433
print(f"Odsetek zarobków wynoszących więcej niż 50000 wśród dolnych outlierów: {lower_high_income/lower_count}")
## Odsetek zarobków wynoszących więcej niż 50000 wśród dolnych outlierów: 0.12408759124087591
Widzimy tutaj zależność, której można się domyślić intuicyjnie - osoby więcej pracujące znacznie częsciej zarabiają więcej - proste pozbycie się tych outlierów mogłoby pozbawić przyszłe modele wartościowych informacji. By zredukować ewentualny negatywny wpływ wartości odstających wykorzystana zostanie transformacja logarytmiczna.
income_data = income_data.copy()
income_data['hours-per-week-log'] = np.log1p(income_data['hours-per-week'])
income_data = income_data.drop('hours-per-week', axis=1)
Zmienna objaśniania zostanła zmapowana na wartości zero i jeden.
income_data['income'] = income_data['income'] == '>50K'
income_data['income'] = income_data['income'].astype(int)
income_data.isna().sum()
## age 0
## workclass 2773
## fnlwgt 0
## educational-num 0
## marital-status 0
## occupation 2783
## race 0
## gender 0
## income 0
## native-american 847
## capital-net 0
## hours-per-week-log 0
## dtype: int64
missing_mask = income_data.isnull().any(axis=1)
income_data_with_missing = income_data[missing_mask]
len(income_data_with_missing) / len(income_data)
## 0.07416832174776564
Obserwacje z brakami stanowią 7% całego zbioru danych, jest to niewielki odsetek, więc obserwacje z brakami zostaną odrzucone.
income_data = income_data.dropna()
W celu wyboru zmiennych, które trafią do ostatecznego zbioru danych, gotowego do klasyfikacji zbadana zostanie korelacja pomiędzy zmiennymi.
corr_income_data = income_data.copy()
for col in categorical_cols:
corr_income_data[col], mapping = pd.factorize(corr_income_data[col])
corr_income_data
## age workclass fnlwgt ... native-american capital-net hours-per-week-log
## 0 25 0 226802 ... 0 0.000000 3.713572
## 1 38 0 89814 ... 0 0.000000 3.931826
## 2 28 1 336951 ... 0 0.000000 3.713572
## 3 44 0 160323 ... 0 8.947546 3.713572
## 5 34 0 198693 ... 0 0.000000 3.433987
## ... ... ... ... ... ... ... ...
## 48837 27 0 257302 ... 0 0.000000 3.663562
## 48838 40 0 154374 ... 0 0.000000 3.713572
## 48839 58 0 151910 ... 0 0.000000 3.713572
## 48840 22 0 201490 ... 0 0.000000 3.044522
## 48841 52 2 287927 ... 0 9.617471 3.713572
##
## [44751 rows x 12 columns]
correlation_methods = {
'nominal:nominal': cramer_v_two_cols,
'nominal:ordinal': cramer_v_two_cols,
'nominal:quantitative': point_biserial,
'ordinal:nominal': cramer_v_two_cols,
'ordinal:ordinal': spearman,
'ordinal:quantitative': spearman,
'quantitative:nominal': point_biserial,
'quantitative:ordinal': spearman,
'quantitative:quantitative': pearson
}
quantitive_cols = ['age', 'fnlwgt', 'capital-net', 'hours-per-week-log']
def get_different_correlations(corr_income_data):
corr_dict = {}
for col in corr_income_data.columns:
for col_2 in corr_income_data.columns:
col_str = ""
if col in quantitive_cols:
col_str += "quantitative"
if col in ordinal_cols:
col_str += "ordinal"
if col in categorical_cols:
col_str += "nominal"
if col_2 in quantitive_cols:
col_str += ":quantitative"
if col_2 in ordinal_cols:
col_str += ":ordinal"
if col_2 in categorical_cols:
col_str += ":nominal"
corr_dict[(col,col_2)] = abs(correlation_methods[col_str](corr_income_data[col], corr_income_data[col_2]))
return corr_dict
corr_dict = get_different_correlations(corr_income_data)
corr_matrix = dict_to_corr_matrix(corr_dict)
def plot_corr_heatmap(corr_matrix):
# Tworzenie heatmapy
plt.figure(figsize=(10, 8))
sns.heatmap(corr_matrix,
annot=True,
fmt=".2f",
cbar=True,
square=True)
plt.title("Korelacja pomiędzy zmiennymi", fontsize=16)
plt.show()
plot_corr_heatmap(corr_matrix)
Zauważamy, że duża część zmiennych nie wykazuje korelacji ze zmienną objaśnianą - income. Wobec tego zostaną one usunięte ze zbioru danych.
corr_income_data = corr_income_data.drop(['fnlwgt', 'native-american','race', 'workclass'],axis = 1)
corr_dict = get_different_correlations(corr_income_data)
corr_matrix = dict_to_corr_matrix(corr_dict)
plot_corr_heatmap(corr_matrix)
Po wstępnym odrzuceniu zmiennych ze względu na niską korelację ze zmienną objaśnianą należy odrzucić zmienne, objaśniane, które wykazują zbyt dużą korelację między sobą. Wartość rzucająca się w oczy to 0.47 między marital-status i age, jednak te zmienne wykazują jedne z wyższych korelacji ze zmienną income, mogą okazać się bardzo ważne w kontekście jakości prognoz. Pozostaną więc w ostatecznym zbiorze.
income_data = income_data.drop(['fnlwgt', 'native-american','race', 'workclass'],axis = 1)
Wszystkie zmienne kategoryczne zostaną zakodowane w celu wydajniejszego operowania nimi podczas badania.
income_data = apply_one_hot_encoding(income_data, ['occupation', 'marital-status'])
income_data['male'] = income_data['gender'] == 'Male'
income_data['male'] = income_data['male'].astype(int)
income_data = income_data.drop('gender', axis = 1)
income_data = undersample_data(income_data, 'income')
income_data.to_csv("prepared_income.csv")
W ramach badania zastosowano trzy techniki uczenia maszynowego: algorytm k-NN, regresję logistyczną oraz metodę lasów losowych. Dla każdej z tych metod przeprowadzono estymację modelu z podstawowymi parametrami, a następnie za pomocą GridSearchCV przeprowadzono wyszukiwanie optymalnych parametrów. Po znalezieniu najlepszych parametrów dla każdej techniki dokonano wyboru najlepszego modelu i przeprowadzono ocenę jego jakości na zbiorach uczącym oraz testowym.
Celem analizy jest ocena, jak dobrze model radzi sobie z klasyfikowaniem danych na podstawie najlepszych parametrów.
Predykcja
Pierwszym krokiem w procesie oceny modelu jest wykonanie predykcji na dwóch zbiorach danych:
Metryki oceny modelu
W kolejnym kroku obliczane są istotne metryki oceny dla obu zbiorów danych:
Metryki te dostarczają wszechstronnych informacji o jakości klasyfikacji, zarówno pod kątem poprawności przewidywań, jak i radzenia sobie z nierównowagą klas.
Macierz konfuzji
Dla obu zbiorów danych generowana jest macierz konfuzji, która przedstawia liczbę:
Macierz konfuzji umożliwia bardziej szczegółową analizę jakości klasyfikacji.
Wizualizacja macierzy konfuzji
Macierze konfuzji są przedstawiane w formie wykresów, które wizualnie prezentują jakość klasyfikacji. Pozwalają one łatwo ocenić skuteczność modelu w rozpoznawaniu pozytywnych i negatywnych próbek w obu zbiorach danych.
Po wykonaniu tych kroków, w tym obliczeniu metryk oraz wygenerowaniu wizualizacji, uzyskujemy kompleksowy obraz skuteczności modelu. Taka analiza umożliwia porównanie jakości działania modelu na zbiorze uczącym i testowym oraz ocenę jego zdolności do generalizowania na nowe dane.
Krzywa ROC i wartość AUC
Dodatkowo aby ocenić jakość naszego najlepszego modelu, wykorzystano wykres ROC (Receiver Operating Characteristic).Na wykresie oś X reprezentuje False Positive Rate (FPR), czyli stosunek fałszywych pozytywnych wyników do wszystkich negatywnych przypadków, a oś Y reprezentuje True Positive Rate (TPR), czyli stosunek prawdziwych pozytywnych wyników do wszystkich pozytywnych przypadków. Im wyższa krzywa i im bardziej znajduje się w lewym górnym rogu wykresu, tym lepszy jest model.
Dodatkowo, AUC (Area Under the Curve) to miara powierzchni pod krzywą ROC, która daje ogólną ocenę jakości modelu. AUC mieści się w przedziale od 0 do 1, gdzie wartość bliska 1 wskazuje na bardzo dobry model, a wartość 0.5 oznacza model, który działa na poziomie losowego zgadywania.
data = pd.read_csv("prepared_income.csv", sep=',')
data = data.drop(columns = 'Unnamed: 0')
income_column = data.pop('income')
data['income'] = income_column
data
## age educational-num capital-net ... Separated male income
## 0 37 9 0.000000 ... 0.0 1 0
## 1 39 13 0.000000 ... 0.0 0 1
## 2 23 9 0.000000 ... 1.0 0 0
## 3 35 13 0.000000 ... 0.0 0 0
## 4 46 13 0.000000 ... 1.0 0 0
## ... ... ... ... ... ... ... ...
## 22189 45 14 0.000000 ... 0.0 1 1
## 22190 34 10 0.000000 ... 0.0 0 1
## 22191 17 8 0.000000 ... 0.0 1 0
## 22192 69 9 0.000000 ... 0.0 1 0
## 22193 29 11 8.947546 ... 0.0 1 1
##
## [22194 rows x 23 columns]
Dzielimy nasz zbiór danych na zbiór uczący i zbiór testowy w proporcji. Dane uczące będą stanowiłły 80% całego zbioru.
X = data.iloc[:, :-1]
y = data.income
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
Przeprowadzamy standaryzacje danych. Okaże się ona pomocna przy stosowaniu metod regresji logistycznej i KNN
# Standaryzacja danych
columns_to_scale = ['age', 'educational-num', 'hours-per-week-log', 'capital-net']
# Tworzenie kopii tylko dla kolumn do skalowania
X_train_scaled = X_train.copy()
X_test_scaled = X_test.copy()
# Skalowanie tylko wybranych kolumn
scaler = StandardScaler()
X_train_scaled[columns_to_scale] = scaler.fit_transform(X_train[columns_to_scale])
X_test_scaled[columns_to_scale] = scaler.transform(X_test[columns_to_scale])
Algorytm K-Nearest Neighbors (KNN) jest jedną z najprostszych i najbardziej intuicyjnych metod klasyfikacji w uczeniu maszynowym. Polega na przypisaniu klasy obiektu na podstawie klas jego najbliższych sąsiadów w przestrzeni cech. Zasada działania KNN opiera się na mierzeniu odległości pomiędzy punktami w przestrzeni cech, a następnie klasyfikowaniu obiektu do tej samej klasy, do której należy większość jego sąsiadów.
# Trening modelu
knn_model_basic = KNeighborsClassifier(n_neighbors=3)
knn_model_basic.fit(X_train_scaled, y_train)
KNeighborsClassifier(n_neighbors=3)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
KNeighborsClassifier(n_neighbors=3)
# Predykcja na zbiorze uczącym
y_train_pred_knn_basic = knn_model_basic.predict(X_train_scaled)
# Predykcja na zbiorze testowym
y_test_pred_knn_basic = knn_model_basic.predict(X_test_scaled)
# Obliczenie miar dla zbioru uczącego
accuracy_knn_train = accuracy_score(y_train, y_train_pred_knn_basic)
recall_knn_train = recall_score(y_train, y_train_pred_knn_basic)
specificity_knn_train = recall_score(y_train, y_train_pred_knn_basic, pos_label=0) # Specificity = TN / (TN + FP)
# Obliczenie miar dla zbioru testowego
accuracy_knn_test = accuracy_score(y_test, y_test_pred_knn_basic)
recall_knn_test = recall_score(y_test, y_test_pred_knn_basic)
specificity_knn_test = recall_score(y_test, y_test_pred_knn_basic, pos_label=0) # Specificity = TN / (TN + FP)
# Wyświetlenie metryk w tabeli
metrics_data_knn = {
'Metric': ['Accuracy', 'Recall', 'Specificity'],
'Train': [accuracy_knn_train, recall_knn_train, specificity_knn_train],
'Test': [accuracy_knn_test, recall_knn_test, specificity_knn_test]
}
metrics_df_knn = pd.DataFrame(metrics_data_knn)
print(metrics_df_knn)
## Metric Train Test
## 0 Accuracy 0.870572 0.779455
## 1 Recall 0.878962 0.785357
## 2 Specificity 0.862143 0.773661
# Wyświetlenie macierzy konfuzji
conf_matrix_knn_train = confusion_matrix(y_train, y_train_pred_knn_basic)
conf_matrix_knn_test = confusion_matrix(y_test, y_test_pred_knn_basic)
# Tworzenie wykresów dla macierzy konfuzji
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
# Wyświetlanie macierzy konfuzji dla zbioru uczącego
ConfusionMatrixDisplay(confusion_matrix=conf_matrix_knn_train).plot(cmap='Blues', ax=ax1);
ax1.set_title('Confusion Matrix - Train Set')
# Wyświetlanie macierzy konfuzji dla zbioru testowego
ConfusionMatrixDisplay(confusion_matrix=conf_matrix_knn_test).plot(cmap='Blues', ax=ax2);
ax2.set_title('Confusion Matrix - Test Set')
plt.tight_layout()
plt.show()
plt.close(fig)
Model posiada dokładność i specyficzność na poziomie 77%. Czułość oscyluje w granicach 78%. Lepsze wyniki na zbiorze treningowym mogą świadczyć o nieznacznym przeuczeniu.
W algorytmie KNN kluczowym elementem wpływającym na jakość klasyfikacji są wybrane parametry. Aby uzyskać najlepsze wyniki, należy dostosować odpowiednio kilka z nich. Przeprowadzono wyszukiwanie najlepszych parametrów KNN, aby zoptymalizować jego działanie na danych.
Parametry do przeszukania:
n_neighbors (Liczba sąsiadów)
Parametr ten określa, ilu sąsiadów będzie branych pod uwagę przy
klasyfikacji nowego obiektu. Optymalna wartość zależy od charakterystyki
danych. Zbyt mała liczba sąsiadów może prowadzić do nadmiernego
dopasowania modelu (przeuczenie), podczas gdy zbyt duża liczba może
spowodować zbyt ogólne przypisanie klasy (niedouczenie).
W naszym przypadku zrezygnowaliśmy z tego parametru w bieżącej wersji,
ponieważ nasz wybór parametrów koncentruje się na innych
aspektach.
metric (Metryka odległości)
Metryka odległości decyduje o tym, jak mierzymy podobieństwo pomiędzy
obiektami w przestrzeni cech. Dla KNN typowe metryki to:
euclidean (odległość euklidesowa) –
najczęściej stosowana metryka, odpowiada tradycyjnej odległości w
przestrzeni kartezjańskiej.manhattan (odległość Manhattan) –
sumuje różnice pomiędzy współrzędnymi, może być bardziej odpowiednia dla
danych o nieliniowych zależnościach.minkowski – ogólna forma, która
pozwala na stosowanie różnych wartości parametru p, co daje możliwość
modyfikacji funkcji odległości.weights (Waga sąsiadów)
Parametr ten definiuje sposób ważenia sąsiadów w procesie
klasyfikacji:
uniform – wszyscy sąsiedzi mają równą
wagę.distance – sąsiedzi bliżsi obiektowi
mają większą wagę, co może poprawić wyniki klasyfikacji w przypadku
nierównomiernie rozłożonych danych.p (Parametr p w metryce
Minkowskiego)
Parametr ten określa wartość, która kontroluje sposób obliczania
odległości w metryce Minkowskiego:
p=1 – odpowiada metryce Manhattan,
gdzie odległość oblicza się jako sumę wartości bezwzględnych różnic
pomiędzy współrzędnymi.p=2 – odpowiada metryce Euklidesowej,
gdzie odległość oblicza się jako pierwiastek z sumy kwadratów różnic
pomiędzy współrzędnymi.Wynikiem przeszukiwania będzie zestaw najlepszych parametrów, który pozwoli na uzyskanie najlepszego modelu KNN dla tego zestawu danych.
# Definicja modelu
knn = KNeighborsClassifier()
# Zakres parametrów do przeszukania
param_grid = {
'n_neighbors': [3, 5, 9, 15], # Liczba sąsiadów
'metric': ['euclidean', 'manhattan', 'minkowski'], # Metryka odległości
'weights': ['uniform', 'distance'], # Waga sąsiadów
'p': [1, 2] # Parametr p w metryce Minkowskiego (1 - Manhattan, 2 - Euclidean)
}
# GridSearchCV - wyszukiwanie najlepszych parametrów
grid_search = GridSearchCV(estimator=knn, param_grid=param_grid,verbose=1, cv=5, scoring='accuracy')
# Dopasowanie modelu
grid_search.fit(X_train_scaled, y_train)
GridSearchCV(cv=5, estimator=KNeighborsClassifier(),
param_grid={'metric': ['euclidean', 'manhattan', 'minkowski'],
'n_neighbors': [3, 5, 9, 15], 'p': [1, 2],
'weights': ['uniform', 'distance']},
scoring='accuracy', verbose=1)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. GridSearchCV(cv=5, estimator=KNeighborsClassifier(),
param_grid={'metric': ['euclidean', 'manhattan', 'minkowski'],
'n_neighbors': [3, 5, 9, 15], 'p': [1, 2],
'weights': ['uniform', 'distance']},
scoring='accuracy', verbose=1)KNeighborsClassifier(metric='manhattan', n_neighbors=15, p=1)
KNeighborsClassifier(metric='manhattan', n_neighbors=15, p=1)
# Najlepsze parametry
print("Najlepsze parametry: ", grid_search.best_params_)
## Najlepsze parametry: {'metric': 'manhattan', 'n_neighbors': 15, 'p': 1, 'weights': 'uniform'}
# Najlepszy wynik (dokładność)
print("Najlepszy wynik (accuracy): ", grid_search.best_score_)
## Najlepszy wynik (accuracy): 0.8091241903689103
best_knn_model = grid_search.best_estimator_
# Predykcja na zbiorze uczącym i testowym z najlepszymi parametrami
y_train_pred_knn_best = best_knn_model.predict(X_train_scaled) # Predykcja na zbiorze uczącym
y_test_pred_knn_best = best_knn_model.predict(X_test_scaled) # Predykcja na zbiorze testowym
# Obliczenie metryk dla obu zbiorów
accuracy_train_knn_best = accuracy_score(y_train, y_train_pred_knn_best)
recall_train_knn_best = recall_score(y_train, y_train_pred_knn_best)
specificity_train_knn_best = recall_score(y_train, y_train_pred_knn_best, pos_label=0) # Specificity = TN / (TN + FP)
accuracy_test_knn_best = accuracy_score(y_test, y_test_pred_knn_best)
recall_test_knn_best = recall_score(y_test, y_test_pred_knn_best)
specificity_test_knn_best = recall_score(y_test, y_test_pred_knn_best, pos_label=0)
# Zapisanie wyników w tabeli
metrics_df_knn = pd.DataFrame({
'Metric': ['Accuracy', 'Recall', 'Specificity'],
'Train': [accuracy_train_knn_best, recall_train_knn_best, specificity_train_knn_best],
'Test': [accuracy_test_knn_best, recall_test_knn_best, specificity_test_knn_best]
})
# Wyświetlenie tabeli z metrykami
print(metrics_df_knn)
## Metric Train Test
## 0 Accuracy 0.828724 0.814373
## 1 Recall 0.863340 0.841291
## 2 Specificity 0.793948 0.787946
# Wyświetlenie macierzy konfuzji dla obu zbiorów (uczacy i testowy)
conf_matrix_train_knn_best = confusion_matrix(y_train, y_train_pred_knn_best)
conf_matrix_test_knn_best = confusion_matrix(y_test, y_test_pred_knn_best)
# Tworzenie wykresów dla macierzy konfuzji
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
# Wyświetlanie macierzy konfuzji dla zbioru uczącego
ConfusionMatrixDisplay(confusion_matrix=conf_matrix_train_knn_best).plot(cmap='Blues', ax=ax1);
ax1.set_title('Confusion Matrix - Train Set')
# Wyświetlanie macierzy konfuzji dla zbioru testowego
ConfusionMatrixDisplay(confusion_matrix=conf_matrix_test_knn_best).plot(cmap='Blues', ax=ax2);
ax2.set_title('Confusion Matrix - Test Set')
plt.tight_layout()
plt.show()
Model wykazuje wysoką wydajność zarówno na zbiorze uczącym, jak i testowym, co sugeruje, że dobrze generalizuje do nowych danych. Czułość jest na wysokim poziomie, co oznacza, że model dobrze wykrywa pozytywne przypadki. Swoistość jest nieco niższa, co może wskazywać, że model preferuje klasyfikowanie próbek pozytywnych kosztem negatywnych, ale nie ma to większego wpływu na ogólną jakość modelu, biorąc pod uwagę wyniki.
# Obliczenie prawdopodobieństw klasy pozytywnej dla zbioru testowego
y_prob_knn_test_best = best_knn_model.predict_proba(X_test_scaled)[:, 1]
# Obliczenie krzywej ROC dla zbioru testowego
fpr_knn_test_best, tpr_knn_test_best, thresholds_knn_test_best = roc_curve(y_test, y_prob_knn_test_best)
# Obliczenie AUC dla zbioru testowego
roc_auc_knn_test_best = auc(fpr_knn_test_best, tpr_knn_test_best)
# Wykres krzywej ROC z wypełnieniem pod krzywą
plt.figure(figsize=(8, 6))
# Wypełnienie pod krzywą ROC
plt.fill_between(fpr_knn_test_best, tpr_knn_test_best, color='skyblue', alpha=0.4)
# Wykres krzywej ROC
plt.plot(fpr_knn_test_best, tpr_knn_test_best, color='b', lw=2, label=f'ROC curve (AUC = {roc_auc_knn_test_best:.2f})')
# Linia losowa
plt.plot([0, 1], [0, 1], color='gray', lw=2, linestyle='--')
# Dodanie etykiet
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve - Best KNN Model (Test Set)')
# Dodanie legendy
plt.legend(loc='lower right')
# Dodanie siatki
plt.grid(True)
# Wyświetlenie wykresu
plt.show()
Regresja logistyczna jest metodą klasyfikacyjną używaną do przewidywania prawdopodobieństwa przynależności do jednej z dwóch klas. Zamiast modelować wartość ciągłą, jak w przypadku regresji liniowej, regresja logistyczna wykorzystuje funkcję logistyczną (sigmoid), która przekształca wynik liniowy w prawdopodobieństwo w przedziale [0, 1].
# Trening modelu
log_reg_basic = LogisticRegression(random_state=42)
log_reg_basic.fit(X_train_scaled, y_train)
LogisticRegression(random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
LogisticRegression(random_state=42)
# Predykcja na zbiorze testowym
y_pred_log_reg_basic_test = log_reg_basic.predict(X_test_scaled)
# Predykcja na zbiorze uczącym
y_pred_log_reg_basic_train = log_reg_basic.predict(X_train_scaled)
# Metryki oceny dla zbioru testowego
accuracy_log_reg_test = accuracy_score(y_test, y_pred_log_reg_basic_test)
recall_log_reg_test = recall_score(y_test, y_pred_log_reg_basic_test, pos_label=1)
specificity_log_reg_test = specificity_score(y_test, y_pred_log_reg_basic_test)
auc_score_test = roc_auc_score(y_test, log_reg_basic.predict_proba(X_test_scaled)[:, 1])
# Metryki oceny dla zbioru uczącego
accuracy_log_reg_train = accuracy_score(y_train, y_pred_log_reg_basic_train)
recall_log_reg_train = recall_score(y_train, y_pred_log_reg_basic_train, pos_label=1)
specificity_log_reg_train = specificity_score(y_train, y_pred_log_reg_basic_train)
auc_score_train = roc_auc_score(y_train, log_reg_basic.predict_proba(X_train_scaled)[:, 1])
metrics_data = {
'Metric': ['Accuracy', 'Recall', 'Specificity', 'AUC'],
'Train': [accuracy_log_reg_train, recall_log_reg_train, specificity_log_reg_train, auc_score_train],
'Test': [accuracy_log_reg_test, recall_log_reg_test, specificity_log_reg_test, auc_score_test]
}
metrics_df = pd.DataFrame(metrics_data)
print(metrics_df)
## Metric Train Test
## 0 Accuracy 0.801690 0.808515
## 1 Recall 0.829063 0.824920
## 2 Specificity 0.774190 0.792411
## 3 AUC 0.882852 0.890313
# Wyświetlenie macierzy konfuzji
conf_matrix_log_reg_train = confusion_matrix(y_train, y_pred_log_reg_basic_train)
conf_matrix_log_reg_test = confusion_matrix(y_test, y_pred_log_reg_basic_test)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
# Macierz konfuzji dla zbioru uczącego
ConfusionMatrixDisplay(confusion_matrix=conf_matrix_log_reg_train).plot(cmap='Blues', ax=ax1);
ax1.set_title('Confusion Matrix - Train')
# Macierz konfuzji dla zbioru testowego
ConfusionMatrixDisplay(confusion_matrix=conf_matrix_log_reg_test).plot(cmap='Blues', ax=ax2);
ax2.set_title('Confusion Matrix - Test')
plt.tight_layout()
plt.show()
W przypadku regresji liniowej model z podstawowymi parametrami charakteryzuje się lepszymi warotściami miar w porównaniu z metodą KNN. Wyniki na zbiorze uczącym i testowym świadczą o dobrym generalizowaniu danych.
Parametry, które badamy w tym przypadku to:
C – odwrotność siły regularizacji:
C kontroluje stopień karania
dużych współczynników modelu. Mniejsza wartość
C oznacza silniejszą regularizację, co
pomaga uniknąć przeuczenia, ale może ograniczać elastyczność modelu.
Większa wartość C umożliwia modelowi
lepsze dopasowanie do danych, jednak może prowadzić do przeuczenia,
zwłaszcza gdy dane są szumne.max_iter – liczba iteracji w procesie
dopasowywania:
max_iter określa maksymalną
liczbę iteracji, które algorytm będzie wykonywał w procesie
optymalizacji. Wyższa wartość max_iter
pozwala algorytmowi na dłuższą pracę, co może być przydatne w przypadku
bardziej złożonych danych, które wymagają więcej cykli, by uzyskać
optymalne wyniki. Zbyt mała wartość może spowodować, że algorytm nie
osiągnie zbieżności.# Parametry do przeszukania
# Parametry do przeszukania
param_grid = {
'C': [0.1, 1, 10], # Inverse of regularization strength
'max_iter': [1000, 3000, 5000], # Liczba iteracji w procesie dopasowywania
}
# Tworzenie obiektu LogisticRegression
log_reg_model = LogisticRegression(random_state=42)
# GridSearchCV do wyszukiwania najlepszych parametrów
grid_search = GridSearchCV(log_reg_model, param_grid, cv=None, verbose=4, scoring='accuracy', n_jobs=7)
grid_search.fit(X_train, y_train)
GridSearchCV(estimator=LogisticRegression(random_state=42), n_jobs=7,
param_grid={'C': [0.1, 1, 10], 'max_iter': [1000, 3000, 5000]},
scoring='accuracy', verbose=4)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. GridSearchCV(estimator=LogisticRegression(random_state=42), n_jobs=7,
param_grid={'C': [0.1, 1, 10], 'max_iter': [1000, 3000, 5000]},
scoring='accuracy', verbose=4)LogisticRegression(C=10, max_iter=1000, random_state=42)
LogisticRegression(C=10, max_iter=1000, random_state=42)
# Najlepsze parametry
best_params = grid_search.best_params_
best_score = grid_search.best_score_
# Model z najlepszymi parametrami
best_log_reg_model = LogisticRegression(**best_params, random_state=42)
best_log_reg_model.fit(X_train, y_train)
LogisticRegression(C=10, max_iter=1000, random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
LogisticRegression(C=10, max_iter=1000, random_state=42)
print('Best parameters from GridSearchCV:', best_params)
## Best parameters from GridSearchCV: {'C': 10, 'max_iter': 1000}
Podobnie jak poprzednio ocenimy model z najlepszymi parametrami
# Predykcja na zbiorze testowym
y_pred_log_reg_best_test = best_log_reg_model.predict(X_test)
# Predykcja na zbiorze uczącym
y_pred_log_reg_best_train = best_log_reg_model.predict(X_train)
# Obliczenie miar dla zbioru testowego
accuracy_log_reg_best_test = accuracy_score(y_test, y_pred_log_reg_best_test)
recall_log_reg_best_test = recall_score(y_test, y_pred_log_reg_best_test)
specificity_log_reg_best_test = recall_score(y_test, y_pred_log_reg_best_test, pos_label=0) # Specificity = TN / (TN + FP)
# Obliczenie miar dla zbioru uczącego
accuracy_log_reg_best_train = accuracy_score(y_train, y_pred_log_reg_best_train)
recall_log_reg_best_train = recall_score(y_train, y_pred_log_reg_best_train)
specificity_log_reg_best_train = recall_score(y_train, y_pred_log_reg_best_train, pos_label=0) # Specificity = TN / (TN + FP)
# Wyświetlenie metryk w tabeli
metrics_data_log_reg_best = {
'Metric': ['Accuracy', 'Recall', 'Specificity'],
'Train': [accuracy_log_reg_best_train, recall_log_reg_best_train, specificity_log_reg_best_train],
'Test': [accuracy_log_reg_best_test, recall_log_reg_best_test, specificity_log_reg_best_test]
}
metrics_df_log_reg_best = pd.DataFrame(metrics_data_log_reg_best)
print(metrics_df_log_reg_best)
## Metric Train Test
## 0 Accuracy 0.802197 0.808515
## 1 Recall 0.829962 0.824920
## 2 Specificity 0.774303 0.792411
# Wyświetlenie macierzy konfuzji
conf_matrix_log_reg_best_train = confusion_matrix(y_train, y_pred_log_reg_best_train)
conf_matrix_log_reg_best_test = confusion_matrix(y_test, y_pred_log_reg_best_test)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
# Macierz konfuzji dla zbioru uczącego
ConfusionMatrixDisplay(confusion_matrix=conf_matrix_log_reg_best_train).plot(cmap='Blues', ax=ax1);
ax1.set_title('Confusion Matrix - Train')
# Macierz konfuzji dla zbioru testowego
ConfusionMatrixDisplay(confusion_matrix=conf_matrix_log_reg_best_test).plot(cmap='Blues', ax=ax2);
ax2.set_title('Confusion Matrix - Test')
plt.show()
Wyszukanie najlepszych parametrow tylko nieznacznie zmieniło wartości miar.
# Obliczenie prawdopodobieństw klasy pozytywnej dla zbioru testowego
y_prob_log_reg_best_test = best_log_reg_model.predict_proba(X_test)[:, 1]
# Obliczenie krzywej ROC dla zbioru testowego
fpr_log_reg_test, tpr_log_reg_test, thresholds_log_reg_test = roc_curve(y_test, y_prob_log_reg_best_test)
# Obliczenie AUC dla zbioru testowego
roc_auc_log_reg_test = auc(fpr_log_reg_test, tpr_log_reg_test)
# Wykres krzywej ROC
plt.figure(figsize=(8, 6))
plt.plot(fpr_log_reg_test, tpr_log_reg_test, color='b', lw=2, label=f'ROC curve (AUC = {roc_auc_log_reg_test:.2f})')
plt.fill_between(fpr_log_reg_test, tpr_log_reg_test, color='skyblue', alpha=0.4) # Wypełnienie pod krzywą
plt.plot([0, 1], [0, 1], color='gray', lw=2, linestyle='--') # Linia losowa
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve - Best Logistic Regression Model (Test Set)')
plt.legend(loc='lower right')
plt.grid(True)
plt.show()
W tym przypadku wartość AUC wynosi 89%.
Random Forest (Las losowy) to jedna z najpopularniejszych i najpotężniejszych metod w uczeniu maszynowym, stosowana głównie do problemów klasyfikacyjnych i regresyjnych. Jest to metoda zespołowa, która łączy wyniki wielu drzew decyzyjnych w celu uzyskania bardziej dokładnych i stabilnych prognoz. Każde drzewo w lesie jest trenowane na losowej próbce danych, co sprawia, że model jest odporny na przeuczenie (overfitting) i dobrze radzi sobie z danymi o dużej zmienności.
Podstawową ideą Random Forest jest:
# Trening modelu
rf_model_basic = RandomForestClassifier(random_state=42)
rf_model_basic.fit(X_train, y_train)
RandomForestClassifier(random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
RandomForestClassifier(random_state=42)
# Predykcja na zbiorze testowym
y_pred_rf_basic_test = rf_model_basic.predict(X_test)
# Predykcja na zbiorze uczącym
y_pred_rf_basic_train = rf_model_basic.predict(X_train)
# Metryki oceny dla zbioru testowego
accuracy_rf_test = accuracy_score(y_test, y_pred_rf_basic_test)
recall_rf_test = recall_score(y_test, y_pred_rf_basic_test, pos_label=1)
specificity_rf_test = specificity_score(y_test, y_pred_rf_basic_test)
# Metryki oceny dla zbioru uczącego
accuracy_rf_train = accuracy_score(y_train, y_pred_rf_basic_train)
recall_rf_train = recall_score(y_train, y_pred_rf_basic_train, pos_label=1)
specificity_rf_train = specificity_score(y_train, y_pred_rf_basic_train)
# Wyświetlenie metryk w tabeli
metrics_data_rf = {
'Metric': ['Accuracy', 'Recall', 'Specificity'],
'Train': [accuracy_rf_train, recall_rf_train, specificity_rf_train],
'Test': [accuracy_rf_test, recall_rf_test, specificity_rf_test]
}
metrics_df_rf = pd.DataFrame(metrics_data_rf)
print(metrics_df_rf)
## Metric Train Test
## 0 Accuracy 0.953027 0.795900
## 1 Recall 0.963924 0.797181
## 2 Specificity 0.942080 0.794643
# Wyświetlenie macierzy konfuzji
conf_matrix_rf_train = confusion_matrix(y_train, y_pred_rf_basic_train)
conf_matrix_rf_test = confusion_matrix(y_test, y_pred_rf_basic_test)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
# Macierz konfuzji dla zbioru uczącego
ConfusionMatrixDisplay(confusion_matrix=conf_matrix_rf_train).plot(cmap='Blues', ax=ax1);
ax1.set_title('Confusion Matrix - Train')
# Macierz konfuzji dla zbioru testowego
ConfusionMatrixDisplay(confusion_matrix=conf_matrix_rf_test).plot(cmap='Blues', ax=ax2);
ax2.set_title('Confusion Matrix - Test')
plt.tight_layout()
plt.show()
W przypadku parametrów bazowych model nie osiąga zbyt satysfakconujących wyników
Parametry do przeszukania w Random Forest
n_estimators (Liczba drzew)
Parametr ten określa, ile drzew decyzyjnych zostanie zbudowanych w
lesie. Większa liczba drzew może poprawić dokładność modelu, ponieważ
redukuje wariancję i zwiększa stabilność prognoz. Jednakże, zbyt duża
liczba drzew może prowadzić do dłuższego czasu treningu, bez znaczącej
poprawy wyników. W naszym przypadku testujemy wartości 10, 50 i 100
drzew.
max_depth (Maksymalna głębokość
drzewa)
Parametr ten kontroluje maksymalną głębokość każdego drzewa w lesie.
Większa głębokość pozwala modelowi uchwycić bardziej złożone zależności
w danych, ale może prowadzić do przeuczenia (overfitting) w przypadku
danych o wysokiej wariancji. Z drugiej strony, zbyt mała głębokość może
skutkować niedouczeniem (underfitting). Testujemy głębokości:
None (brak ograniczenia), 10, 20 i 30.
min_samples_split (Minimalna liczba próbek
do podziału węzła)
Parametr ten określa minimalną liczbę próbek, które muszą być obecne w
węźle, aby mogło dojść do podziału. Wyższa wartość zmniejsza liczbę
podziałów w drzewach, co pomaga uniknąć przeuczenia, ale może także
obniżyć zdolność modelu do uchwycenia subtelnych zależności w danych.
Testujemy wartości: 2, 5 i 10.
min_samples_leaf (Minimalna liczba próbek w
liściu)
Parametr ten kontroluje minimalną liczbę próbek, które muszą znajdować
się w liściu drzewa. Zwiększenie tej liczby może zmniejszyć wariancję
modelu, eliminując bardzo małe i potencjalnie zbyt specyficzne liście.
Zmniejsza to ryzyko przeuczenia. Testujemy wartości: 1, 2 i 4.
# Parametry do przeszukania
param_grid = {
'n_estimators': [10, 50, 100], # Liczba drzew
'max_depth': [None, 10, 20, 30], # Maksymalna głębokość drzewa
'min_samples_split': [2, 5, 10], # Minimalna liczba próbek do podziału
'min_samples_leaf': [1, 2, 4] # Minimalna liczba próbek w liściu
}
# Tworzenie obiektu RandomForestClassifier
rf_model = RandomForestClassifier(random_state=42)
# GridSearchCV do wyszukiwania najlepszych parametrów
grid_search = GridSearchCV(rf_model, param_grid, cv=None, verbose=4,scoring='accuracy', n_jobs=7)
grid_search.fit(X_train, y_train)
GridSearchCV(estimator=RandomForestClassifier(random_state=42), n_jobs=7,
param_grid={'max_depth': [None, 10, 20, 30],
'min_samples_leaf': [1, 2, 4],
'min_samples_split': [2, 5, 10],
'n_estimators': [10, 50, 100]},
scoring='accuracy', verbose=4)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. GridSearchCV(estimator=RandomForestClassifier(random_state=42), n_jobs=7,
param_grid={'max_depth': [None, 10, 20, 30],
'min_samples_leaf': [1, 2, 4],
'min_samples_split': [2, 5, 10],
'n_estimators': [10, 50, 100]},
scoring='accuracy', verbose=4)RandomForestClassifier(max_depth=20, min_samples_leaf=4, min_samples_split=10,
random_state=42)RandomForestClassifier(max_depth=20, min_samples_leaf=4, min_samples_split=10,
random_state=42)# Najlepsze parametry
best_params = grid_search.best_params_
best_score = grid_search.best_score_
# Model z najlepszymi parametrami
best_rf_model = RandomForestClassifier(grid_search.best_estimator_)
print('Best parameters from GridSearchCV:', best_params)
## Best parameters from GridSearchCV: {'max_depth': 20, 'min_samples_leaf': 4, 'min_samples_split': 10, 'n_estimators': 100}
# Najlepszy model z GridSearchCV (już wytrenowany)
best_rf_model = grid_search.best_estimator_
# Predykcja na zbiorze testowym
y_pred_rf_best_test = best_rf_model.predict(X_test)
# Predykcja na zbiorze uczącym
y_pred_rf_best_train = best_rf_model.predict(X_train)
# Obliczenie miar dla zbioru testowego
accuracy_rf_best_test = accuracy_score(y_test, y_pred_rf_best_test)
recall_rf_best_test = recall_score(y_test, y_pred_rf_best_test)
specificity_rf_best_test = recall_score(y_test, y_pred_rf_best_test, pos_label=0) # Specificity = TN / (TN + FP)
# Obliczenie miar dla zbioru uczącego
accuracy_rf_best_train = accuracy_score(y_train, y_pred_rf_best_train)
recall_rf_best_train = recall_score(y_train, y_pred_rf_best_train)
specificity_rf_best_train = recall_score(y_train, y_pred_rf_best_train, pos_label=0) # Specificity = TN / (TN + FP)
# Wyświetlenie metryk w tabeli
metrics_data_rf_best = {
'Metric': ['Accuracy', 'Recall', 'Specificity'],
'Train': [accuracy_rf_best_train, recall_rf_best_train, specificity_rf_best_train],
'Test': [accuracy_rf_best_test, recall_rf_best_test, specificity_rf_best_test]
}
metrics_df_rf_best = pd.DataFrame(metrics_data_rf_best)
print(metrics_df_rf_best)
## Metric Train Test
## 0 Accuracy 0.852154 0.830818
## 1 Recall 0.890425 0.857208
## 2 Specificity 0.813707 0.804911
# Wyświetlenie macierzy konfuzji
conf_matrix_rf_best_train = confusion_matrix(y_train, y_pred_rf_best_train)
conf_matrix_rf_best_test = confusion_matrix(y_test, y_pred_rf_best_test)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
# Macierz konfuzji dla zbioru uczącego
ConfusionMatrixDisplay(confusion_matrix=conf_matrix_rf_best_train).plot(cmap='Blues', ax=ax1);
ax1.set_title('Confusion Matrix - Train')
# Macierz konfuzji dla zbioru testowego
ConfusionMatrixDisplay(confusion_matrix=conf_matrix_rf_best_test).plot(cmap='Blues', ax=ax2);
ax2.set_title('Confusion Matrix - Test')
plt.show()
Wyniki lasu losowego prezentują się bardzo dobrze, z wszystkimi miarami (dokładność, recall, specificity) przekraczającymi 80%. Oznacza to, że model radzi sobie bardzo skutecznie zarówno na danych treningowych, jak i testowych, co sugeruje jego dobrą generalizację. Dzięki wysokiej czułości i specyficzności model wykazuje solidne zdolności do wykrywania zarówno pozytywnych, jak i negatywnych przypadków,
# Obliczenie prawdopodobieństw klasy pozytywnej dla zbioru testowego
y_prob_rf_best_test = best_rf_model.predict_proba(X_test)[:, 1]
# Obliczenie krzywej ROC dla zbioru testowego
fpr_rf_test, tpr_rf_test, thresholds_rf_test = roc_curve(y_test, y_prob_rf_best_test)
# Obliczenie AUC dla zbioru testowego
roc_auc_rf_test = auc(fpr_rf_test, tpr_rf_test)
# Wykres krzywej ROC
plt.figure(figsize=(8, 6))
plt.plot(fpr_rf_test, tpr_rf_test, color='b', lw=2, label=f'ROC curve (AUC = {roc_auc_rf_test:.2f})')
plt.fill_between(fpr_rf_test, tpr_rf_test, color='skyblue', alpha=0.4) # Wypełnienie pod krzywą
plt.plot([0, 1], [0, 1], color='gray', lw=2, linestyle='--') # Linia losowa
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve - Best Random Forest Model (Test Set)')
plt.legend(loc='lower right')
plt.grid(True)
plt.show()
Model stworzony na bazie Lasu losowego prezentuje najlepszą dokłądność z wartością AUC na poziomie 92%.
# Ocena ważności zmiennych
importances = best_rf_model.estimators_[0].feature_importances_
feature_names = X_train.columns # Upewnij się, że masz nazwane kolumny w danych
importance_df = pd.DataFrame({'Feature': feature_names, 'Importance': importances})
importance_df = importance_df.sort_values(by='Importance', ascending=False)
# Wizualizacja ważności zmiennych
plt.figure(figsize=(10, 6))
plt.barh(importance_df['Feature'], importance_df['Importance'], color='lightgreen')
plt.xlabel('Importance')
plt.ylabel('Features')
plt.title('Feature Importance in Random Forest')
plt.gca().invert_yaxis() # Odwróć oś Y dla lepszej czytelności
plt.show()
Wyniki pokazują, że najbardziej istotne zmienne, takie jak educational-num, capital-net, never-married, separated oraz age, mają znaczący wpływ na prognozowanie wyniku. Może to wskazywać, że wykształcenie, stan majątkowy oraz status cywilny są kluczowe przy przewidywaniu poziomu dochodówczy wiek odgrywają istotną rolę, co jest zgodne z intuicyjnymi zależnościami społecznymi i ekonomicznymi.
# Wykres krzywych ROC dla trzech modeli
plt.figure(figsize=(10, 8))
# Dodanie krzywych ROC do wykresu
plt.plot(fpr_rf_test, tpr_rf_test, color='darkgreen', lw=2, label=f'Random Forest (AUC = {roc_auc_rf_test:.2f})');
plt.plot(fpr_log_reg_test, tpr_log_reg_test, color='darkorange', lw=2, label=f'Logistic Regression (AUC = {roc_auc_log_reg_test:.2f})');
plt.plot(fpr_knn_test_best, tpr_knn_test_best, color='darkblue', lw=2, label=f'KNN (AUC = {roc_auc_knn_test_best:.2f})');
# Linia losowa
plt.plot([0, 1], [0, 1], color='gray', lw=2, linestyle='--');
# Dodanie tytułów i etykiet osi
plt.title('ROC Curves Comparison - All Models (Test Set)', fontsize=16, fontweight='bold');
plt.xlabel('False Positive Rate (FPR)', fontsize=14);
plt.ylabel('True Positive Rate (TPR)', fontsize=14);
# Dodanie legendy
plt.legend(loc='lower right', fontsize=12);
# Dodanie siatki i poprawa wyglądu
plt.grid(True, linestyle='--', alpha=0.7);
# Wyświetlenie wykresu
plt.tight_layout();
plt.show();
Najlepszym modelem okazał się ten zbudowany przez użycie metody lasów losowych
from sklearn.inspection import PartialDependenceDisplay
# Wybieramy tylko te cechy, które są najbardziej istotne
important_features = ['educational-num', 'capital-net']
# Tworzymy wykresy dla wybranych cech
fig, ax = plt.subplots(1, 2, figsize=(14, 6), constrained_layout=True) # Dostosowujemy układ do dwóch cech
for i, axi in enumerate(ax.flat):
if i < len(important_features): # Sprawdzamy, czy istnieje taka cecha
# Wybieramy numer kolumny dla danej cechy w oryginalnym zbiorze danych
feature_idx = X_train_scaled.columns.get_loc(important_features[i])
# Tworzymy wykres zależności częściowej
PartialDependenceDisplay.from_estimator(
best_knn_model,
X_train_scaled[:1000], # Używamy próbki 1000 wierszy
features=[feature_idx], # Numer kolumny cechy
feature_names=X_train_scaled.columns, # Używamy pełnej listy nazw cech
ax=axi,
)
axi.set_title(f"Część. zależność - {important_features[i]}")
plt.show()
Wykonano wykresy zależności częściowych (Partial Dependence Plots, PDP) dla dwóch istotnych zmiennych: educational-num (numer wykształcenia) oraz capital-net (dochód netto). Celem tych wykresów jest zobrazowanie wpływu wybranych cech na przewidywaną zmienną wyjściową (w tym przypadku poziom dochodu) przy stałych wartościach innych zmiennych. Na podstawie analizy wstępnej danych, wybrano dwie cechy, które mają największy wpływ na wynik modelu: educational-num i capital-net. Zmienna educational-num reprezentuje poziom wykształcenia (liczba lat nauki), a capital-net to dochód netto danej osoby. Wykresy te pokazują, jak zmienia się przewidywana wartość modelu (np. poziom dochodu) w zależności od zmiany jednej z cech, podczas gdy inne cechy są trzymane na stałym poziomie.I tak widzimy ze wzrost cech powoduje zwiekszenie potencjalnego dochodu
Celem projektu była analiza skuteczności różnych metod klasyfikacyjnych (K-Nearest Neighbors, Regresja Logistyczna, Random Forest) w kontekście prognozowania danych dotyczących poziomu dochodów. W projekcie zastosowano różne podejścia do optymalizacji modeli oraz dobrania najlepszych parametrów, a także przeanalizowano wyniki uzyskane na zbiorach uczącym i testowym. Pod względem jakości modelu Random Forest okazał się najskuteczniejszy, osiągając najwyższą dokładność oraz wartość AUC. Regresja Logistyczna osiągnęła bardzo wysokie wyniki, jednak nieznacznie gorsze od lasu losowego, podczas gdy K-Nearest Neighbors oferował dobre wyniki, ale wymagał dalszej optymalizacji, aby uzyskać stabilniejszy model. Stworzono również wizualizacje instotności cech w kontekscie lasu losowego oraz KNN, z których wynikało że największy wpływ na dochod ma liczba la tedukacji oraz zysk kapitałowy.
Podsumowanie wyników AUC:
Random Forest okazał się najlepszym modelem do przewidywania poziomu dochodów na podstawie dostępnych cech, co pokazuje jego wyższa odporność na przeuczenie i lepsza generalizacja w porównaniu do innych metod.